現代 Web 服務中,檔案上傳是一個常見的情境。
無論是使用者上傳照片、夾帶附件,檔案上傳都是不可或缺的功能。
本文介紹如何在 Django Ninja 中實現圖片上傳功能,以使用者「上傳大頭貼」(以下都稱為 avatar,因為大頭貼感覺太可愛🥹)API 為例,帶你一步步了解這個過程。
本文所有的程式碼改動,可參考這個 PR。
不過,在此之前,我們要先了解,本章有哪些主題。
對 API 專案而言,進階功能能幫助我們應對複雜場景與大型專案的挑戰。
雖然這是一個入門指南,但我們仍會涵蓋一些常見的進階功能,這些功能不僅提升 API 的靈活性,還能增強了系統效能與使用者體驗。
本章將介紹 3 個重要的進階功能:
這些技術不僅對大型專案至關重要,也讓你能在 API 開發中,有效應對多變的需求。
了解了本章重點後,我們開始講述第一個功能——檔案上傳。
在 Django Ninja 中,我們可以使用UploadedFile
來接收上傳的檔案,它是 Django UploadedFile
的重新封裝,兩者基本上大同小異。
UploadedFile 是 Django 處理檔案上傳的核心物件。當使用者上傳檔案時,Django 會自動將檔案封裝成UploadedFile
實例,方便我們進行後續的檔案處理和儲存。
UploadedFile
物件有很多屬性,其中比較常用的有:
name
:上傳檔案的名稱。這可以用來存取檔案的原始檔名。size
:檔案的大小(以位元組計算)。我們可以用來進行檔案大小的驗證。content_type
:檔案的 MIME 類型。這對於驗證上傳檔案的格式非常有用,比如確保檔案是圖片格式。我們後面會用到!read()
:用來讀取檔案的內容。當需要進行自訂檔案處理時,我們可以使用這個方法來取得檔案的二進位資料。chunks()
:當檔案非常大時,使用這個方法可以分塊讀取檔案,避免過多的記憶體佔用。這些特性使得UploadedFile
非常靈活,能夠應對各種上傳需求,從簡單的圖片上傳到大型檔案的處理。
OK,關於檔案上傳,了解UploadedFile
這個核心元件就足夠了。
開始實作「上傳 avatar」API 的程式碼之前,我們要先進行一些「前置作業」。
檔案上傳,有很多部分其實和 Django 比較有關,而非 Django Ninja 範疇,所以我會重點帶過。
在實作檔案上傳功能之前,我們需要先告訴 Django:如何處理上傳的檔案。這涉及到MEDIA_URL
和MEDIA_ROOT
的設定。
MEDIA_URL
和MEDIA_ROOT
設定MEDIA_URL
:這是檔案的 URL 前綴,所有上傳的檔案會透過這個 URL 來存取。MEDIA_ROOT
:這是 Django 伺服器內部,實際儲存上傳檔案的路徑。在專案的settings.py
新增程式碼如下:
# NinjaForum/settings.py
...
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
如此一來,上傳的檔案會儲存在專案根目錄的media
資料夾中,並通過/media/
路徑來存取。
我們需要 Django 提供的static
方法,來讓開發環境能夠直接存取這些檔案。
在專案的urls.py
加上這行:
# NinjaForum/urls.py
from django.conf import settings
from django.conf.urls.static import static
...
urlpatterns = [
path('admin/', admin.site.urls),
path('', api.urls),
# 讓開發環境可以存取上傳的檔案,僅供開發環境使用
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
這段程式碼允許 Django 在開發環境下提供靜態檔案的存取。
ImageField
欄位ImageField
是 Django 專門用來存放圖片的欄位,它實際上是存儲圖片的檔案路徑。類似的欄位還有FileField
。
程式碼如下:
# user/models.py
class User(AbstractUser):
...
avatar = models.ImageField(upload_to='avatars/', null=True)
搭配之前的settings.py
設定,avatar
欄位會將上傳的圖片存在media/avatars/
資料夾中——這個路徑由MEDIA_ROOT
與欄位upload_to
共同決定。
移至本分支後,專案中已經有新的遷移檔,記得要資料庫遷移:
python manage.py migrate
# 或
make migrate
附帶一提,ImageField
依賴第三方套件——Pillow。這個套件為欄位提供了處理圖片功能,要先安裝欄位才能正常運作:
pip install Pillow
# 或
poetry add pillow
使用 Poetry 的讀者,直接在合併後的分支poetry install
即可。
前置作業結束,我們終於可以進入重頭戲。
以下是完整的「上傳 avatar」功能:
from ninja import File, Router, UploadedFile
from ninja.errors import HttpError
...
@router.post('/users/{int:user_id}/avatar/',summary='上傳 avatar')
def upload_avatar(
request: HttpRequest,
user_id: int,
avatar_file: UploadedFile = File()
) -> dict[str, str]:
"""
上傳 avatar
"""
# 檢查檔案類型
if not avatar_file.content_type.startswith('image/'):
raise HttpError(400, '檔案必須是圖片格式')
user = User.objects.get(id=user_id)
user.avatar = avatar_file
user.save()
return {'detail': '圖片上傳成功'}
以下是對這段程式碼的重點解析。
UploadedFile
參數定義在 view 函式的簽名中,UploadedFile
作為 type hint 使用,而avatar_file
參數則代表上傳的檔案。
你可以任意命名avatar_file
這個參數,比如文件範例中是叫file
。我特地取不同名字,就是要強調它的名字是完全可自訂的。
但是!無論你取什麼名稱,在發請求時,body 中的 key 也要使用相同的名稱。
此時的 HTTP 請求應該是長這樣的:(留意 body 中的avatar_file
)
POST /users/1/avatar/
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
# 以下是 body 內容
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar_file"; filename="example.jpg"
Content-Type: image/jpeg
(binary image data here)
------WebKitFormBoundary7MA4YWxkTrZu0gW--
Header 中的Content-Type
為multipart/form-data
,而且每個 key-value 對都是以Content-Disposition: form-data;
開頭。這種格式允許在一個請求中同時傳送多個不同類型的資料,包括文字和二進位檔案。
其中的細節,可以參考這篇〈multipart/form-data 初探〉。
File
函式定義=File()
的目的是在告訴 Django Ninja,這個參數應該從 HTTP 請求中的「上傳檔案」部分獲取。類似做法還有我們第 11 篇提到的Query
。
如果沒有這個標記,框架可能無法正確識別並處理上傳的內容。
我們使用了UploadedFile
的content_type
屬性,獲得這個 body 內容的檔案類型,確認它是圖片,然後才發行。
# 檢查檔案類型
if not avatar_file.content_type.startswith('image/'):
raise HttpError(400, '檔案必須是圖片格式')
這個方法雖然粗糙,但對於簡單的圖片上傳功能來說已經足夠。
測試上傳純文字文件的結果:
// 400 Bad Request
{
"detail": "檔案必須是圖片格式"
}
在生產環境中,你需要更嚴格的檢查,例如使用專門的圖片處理套件來驗證檔案內容。
最後,我們將圖片賦值給User
的avatar
欄位,並呼叫save()
方法。
Django 會自動處理檔案的儲存,如果名稱重複,它還會自動產生唯一的檔名,然後把檔案放到我們之前指定的位置。
透過 API 上傳了兩次一模一樣的 avatar 後,我們在專案根目錄使用tree
指令看一下結果:
❯ tree media
media
└── avatars
├── my-avatar.png
└── my-avatar_gVwgCiG.png # 相同檔名第二次上傳,自動更名
2 directories, 2 files
可以看到,第二次上傳的圖片被「自動更名」了。這保證了檔名的唯一性。
在生產環境中,最好自行定義檔案的統一命名格式,以確保更好的管理和安全性。
本文介紹了如何在 Django Ninja 中實作檔案上傳功能,從前置設定到 API 實作,詳述了UploadedFile
的使用方式。
接下來,我們將介紹另一項進階功能——分頁(Pagination),它能幫助你在回應大量資料時,有效提升效能與使用者體驗。
本文同步發表於我的部落格——Code and Me